fix(engine): count fnmatch label-pattern wildcard groups per raw star, not by path-glob rules - #10112
Conversation
…, not by path-glob rules labelPatternToRegExp reused change-guardrail's path-glob wildcard-group counter to guard against catastrophic backtracking, but that counter treats a ** pair as ONE group (the path compiler collapses ** into a single .*). The fnmatch compiler here has no ** concept and emits one .* per *, so the count and the compiled regex disagreed for any ** pattern: *a**b counted 2 but compiled 3 .* groups and was wrongly accepted, admitting a pattern this compiler builds into a catastrophic- backtracking RegExp on an adversarial near-miss label. Count one group per raw * (no ** pairing; ? and [..] are not counted) and compare against the shared MAX_GLOB_WILDCARD_GROUPS, now exported from change-guardrail rather than redeclared. An over-complex registry key degrades to the existing LABEL_PATTERN_NEVER_MATCHES and is still cached. The path-glob counter and every path consumer keep their **-is-one-group semantics unchanged. Closes JSONbored#9994
|
Caution 🛑 LoopOver review result - reject/close recommendedReview updated: 2026-07-31 08:09:22 UTC
Review summary Blockers
Nits — 4 non-blocking
Why this is blocked
📋 Copy for AI agents — paste into your coding agentDecision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. Scroll preview
A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10112 +/- ##
==========================================
+ Coverage 91.95% 91.98% +0.02%
==========================================
Files 931 931
Lines 113921 113939 +18
Branches 27504 27511 +7
==========================================
+ Hits 104757 104806 +49
+ Misses 7863 7828 -35
- Partials 1301 1305 +4
Flags with carried forward coverage won't be shown. Click here to find out more.
|
|
LoopOver is closing this pull request on the maintainer's behalf (AI review flagged a likely critical defect: An unrelated binary file, `apps/loopover-ui/public/downloads/loopover-extension.zip`, is added in this diff with no source, no documentation, and no stated connection to the fnmatch-wildcard-counting fix — this is scope creep that must be pulled into its own PR or explained before merge.). This is an automated maintenance action — to pursue this change, please open a new pull request with the issues resolved. Closed PRs may be analyzed later to improve review accuracy, but they are not automatically reopened or re-reviewed. |


What & why
labelPatternToRegExp(packages/loopover-engine/src/scoring/label-match.ts) guards against catastrophic backtracking by counting wildcard groups before compiling a label pattern to a RegExp — reusingchange-guardrail.ts'shasUnsafeWildcardCount. But that predicate is the path-glob counter, which deliberately treats a**pair as one group because the path compiler collapses**into a single.*.The fnmatch compiler here has no
**concept — it emits one.*per*. So the count and the compiled regex disagreed for any**pattern:.*groups compiled*a**b**/**a**b**cThe cap's own benchmark puts 3 groups at "over 2 seconds at ~4,000 chars" and 4 at "35 seconds at 1,614 chars", so these accepted patterns are exactly the ones that risk a catastrophic-backtracking
RegExp.test()on an adversarial near-miss label.labelMatchesPattern's left-hand input is caller-supplied (ScorePreviewInput.labels) and the right-hand patterns are registrylabelMultiplierskeys the module itself documents as untrusted.The fix
Count the groups the fnmatch compiler actually emits — one per raw
*, with no**pairing (?compiles to a single.and[…]classes are not counted, neither can backtrack ambiguously) — and compare againstMAX_GLOB_WILDCARD_GROUPS, now exported fromchange-guardrail.tsso the two surfaces share one empirically-safe threshold rather than redeclaring it (the exact drift the existinghasUnsafeWildcardCountexport comment warns about).An over-complex pattern degrades to the existing
LABEL_PATTERN_NEVER_MATCHESand is still cached, exactly as today — the fail-safe direction here is "no multiplier applies".Unchanged:
hasUnsafeWildcardCount,countWildcardGroups,globToRegExp,matchesAnyand every path-glob consumer keep their**-is-one-group semantics byte-identically (correct for the path compiler — rejectingpublic/**/*.jsonthere would break the content lane). Every ≤2-group label pattern (type:*,kind/*,priority:?,a*b*c,[bc]ug), the[seq]/[!seq]/invalid-range handling, and the LRU cache behaviour are all preserved.Tests
packages/loopover-engine/test/label-match.test.ts, new — node:test per thecontent-lane-flag.test.tsconvention):*a**band**/**are rejected (never match); the ≤2-group and non-*cases still match; a rejected pattern is still cached (repeated read served from the cache).test/unit/scoring.test.ts): the same rejection + preserved cases throughlabelMatchesPattern/labelMultiplierFor. One existing assertion is updated —public/**/*.json(3 compiled groups) is now correctly rejected as a label pattern (it was the path-glob-count's false accept); the comment now states the fnmatch counting rule.mainand pass with the fix.Validation
packages/loopover-engine/src/scoring/label-match.tsand.../signals/change-guardrail.tsis 100% line and branch (engine lines credited via the root-vitest upload; the added test is also inpackages/loopover-engine/test/**for the dual-upload union).npm run typecheckclean for these files;npm run engine-parity:drift-checkpasses; the engine's ownnode --testsuite is green;npm run dead-exports:checkclean.git diff --checkclean; no schema/migration/generated-artifact change.Closes #9994